from turbogears import identity, redirect, visit
from turbogears import expose, controllers
import turbogears
import cherrypy

from openid.consumer import consumer
from openid.consumer.discover import DiscoveryFailure
import urllib2
import urlparse

import logging
log = logging.getLogger(__name__)

def user_info_setter(user, info):
    """
    Attach this to a typical TurboGears User object like so:
    class User(Entity):
       ...
       user_info = property(lambda self: None, user_info_setter)
    """
    if info.has_key('email'):
        user.email_address = info['email']
    if info.has_key('nickname'):
        user.display_name = info['nickname']
    if info.has_key('fullname'):
        user.display_name = info['fullname']

class SQLLiteStoreFactory(object):
    """
    A factory which returns a simple sqlite openid store.  Since sqlite
    requires a separate connection per thread, a new store is created
    for each call on the instance.
    """
    # Consider consider keeping a per-thread cache of stores.

    def __call__(self):
        "Return a store"
        return self.get_cached_store() or self.create()

    def get_cached_store(self):
        "stubbed"
        return None

    def cache_store(self, store):
        "stubbed"

    def create(self):
        log.debug('creating store')
        try:
            import sqlite3 as sqlite
        except ImportError:
            from pysqlite2 import dbapi2 as sqlite
        from openid.store import sqlstore
        con = sqlite.connect('openid.db')
        store = sqlstore.SQLiteStore(con)
        try:
            store.createTables()
        except:
            pass # the tables probably already exist
        self.cache_store(store)
        return store

class OpenIDLoginController(controllers.Controller):
    """
    TODO: complete description
    
    openid_path         The path at which this controller is installed. Defaults to
                        /openid in the app.
    
    store_factory       Supply a callable object which returns (for each call)
                        a sqlstore suitable for storing openid data.  If omitted,
                        uses the SQLLiteStoreFactory.
    """
    
    sreg_uri = "http://openid.net/sreg/1.0"
    """
    fields to include in the sreg parameters
    may include 'nickname,email,fullname,dob,gender,postcode,country,language,timezone'
    """
    sreg_optional = ('nickname', 'fullname', 'email')
    # TODO require this if sreg is used (factor out SREG functionality)
    sreg_policy_url = 'http://www.google.com'
    openid_path = turbogears.url('/openid')
    
    def __init__(self, user_class, visit_identity_class,
                  openid_path = None, store = None,
                  trust_root = None, sreg_optional = None,
                  sreg_policy_url = None,
                  store_factory = None,
                  ):
        self.user_class = user_class
        self.visit_identity_class = visit_identity_class

        if openid_path:
            self.openid_path = openid_path

        if not sreg_optional is None:
            self.sreg_optional = sreg_optional

        if not sreg_policy_url is None:
            self.sreg_policy_url = sreg_policy_url

        if store is not None:
            store_factory = lambda: store
        self.store_factory = store_factory or SQLLiteStoreFactory()

        self._trust_root = trust_root

    def get_trust_root(self):
        base = cherrypy.request.base
        if self._trust_root:
            return urlparse.urljoin(base, self._trust_root)
        return urlparse.urljoin(base, "/")

    trust_root = property(get_trust_root)

    def openId(self, session):
        return consumer.Consumer(session, self.store_factory())

    @expose(template="tgopenidlogin.templates.login")
    def login(self, forward_url = None, msg = None, identity_url = None, *args, **kw):
        if forward_url == "login":
            forward_url = None

        if not forward_url:
            forward_url = cherrypy.request.path

        if not msg:
            if identity.was_login_attempted():
                msg=_("The credentials you supplied were not correct or "
                   "did not grant access to this resource.")
            elif identity.get_identity_errors():
                msg=_("You must provide your credentials before accessing "
                   "this resource.")
            else:
                msg=_("Please log in.")
                forward_url= cherrypy.request.headers.get("Referer", "/")

        parameters = cherrypy.request.params.copy()
        rm_from_param = ["forward_url", "identity_url", "openid", "nonce", "login"]
        for to_rm in rm_from_param:
            if to_rm in parameters:
                del parameters[to_rm]
           
        cherrypy.response.status=403
        return dict(message=msg, logging_in=True,
                    original_parameters=parameters,
                    forward_url=forward_url, openid_path = self.openid_path)

    @expose()
    def logout(self):
        identity.current.logout()
        raise redirect("/")

    @expose()
    def loginBegin(self, identity_url, forward_url = "/", **kw):
        """Called when user clicks OK on the login form."""
        openId = self.openId(cherrypy.session)
        try:
            authrequest = openId.begin(identity_url)
        except DiscoveryFailure, e:
            return self.login(forward_url = forward_url, msg = str(e))

        sreg_optional = ','.join(self.sreg_optional)
        authrequest.addExtensionArg(self.sreg_uri, 'optional', sreg_optional)
        authrequest.addExtensionArg(self.sreg_uri, 'policy_url', self.sreg_policy_url)

        returnUri = self.openIdReturnUri(cherrypy.session, forward_url)

        openIdUri = authrequest.redirectURL(self.trust_root, returnUri)
        raise redirect(openIdUri)

    def openIdReturnUri(self, session, forward_url):
        """Given an OpenID request information object, return a URI on this server."""
        import urllib
        forward_url = urllib.urlencode({'forward_url': forward_url})

        base = cherrypy.request.base
        log.debug('base URL is %s', base)
        # note, if the server is behind a proxy, the TG configuration must use
        # a base_filter_url or base will be incorrect (localhost always) e.g.:
        #base_url_filter.on = True
        #base_url_filter.use_x_forwarded_host = True
        # in CP3, the same setting is controlled by
        #tools.proxy.on = True
        if('://localhost' in base): log.warning('Base URL is coming back as localhost: this may be correct in a testing environment but probably not in production environment. Double-check that the proxy settings are correct')
        openid_path = urlparse.urljoin(base, self.openid_path)

        return '%s/loginFinish?%s' % (openid_path, forward_url)

    @expose()
    def loginFinish(self, forward_url, **kws):
        openId = self.openId(cherrypy.session)

        # instead of using the *kws as passed in, use the
        #  cherrypy.original_params, as they haven't passed through
        #  the cherrypy nested variables filter (installed by TG)
        # note, this also includes forward_url, which is required
        #  by openid
        kws = cherrypy.request.original_params

        # reconstruct the return Uri.  OpenID needs to verify it matches.
        return_to = self.openIdReturnUri(cherrypy.session, forward_url)
        response = openId.complete(kws, return_to)

        if response.status == 'success':
            u = self.user_class.get_by(user_name=kws['openid.identity'])
            if not u:
                u = self.user_class(user_name=kws['openid.identity'])

            user_info = response.extensionResponse(self.sreg_uri, require_signed=False)
            # User object needs to handle this attribute if it wants to do anything
            #  with the data
            u.user_info = user_info

            u.flush()

			# associate this user with this visit
            visit_key = visit.current().key
            link = self.visit_identity_class.get_by(visit_key=visit_key)
            if not link:
                link = self.visit_identity_class(visit_key=visit_key, user=u)
            else:
                link.user = u
            link.flush()

            redirect(forward_url)

        identity.set_login_attempted(True)
        return self.login(forward_url=forward_url)
